package ws.jdbc.concurrent.driver.sqlserver.tds;

import java.io.IOException;
import java.nio.ByteBuffer;
import java.sql.SQLException;
import ws.jdbc.concurrent._development.DevMode;

/**
 * Handles the token stream reading process.
 *
 * @author Wilson Santos, 36228 <wilson.santos@ua.pt>
 */
public abstract class TokenReader {

  /** Buffer to read the token from. */
  protected ByteBuffer stream;
  /** Variable length data stream reader to read some token fields. */
  protected VariableLengthDataStreamReader vldsReader;
  /** Data type dependent data stream reader to read data type information. */
  protected DataTypeDependentDataStreamReader dtddsReader;
  /** Number of bytes of the token. */
  protected short tokenLength;
  /* -------------------------- TOKEN TYPES ---------------------------------- */
  /** Token type: Describes the column information in browse mode , sp_cursoropen, and sp_cursorfetch. */
  static final byte TOKEN_COLINFO = (byte) 0xA5;
  /** Token type: Describes the result set for interpretation of following ROW datastreams. */
  static final byte TOKEN_COLMETADATA = (byte) 0x81;
  /** Token type: Used to send an error message to the client. */
  static final byte TOKEN_ERROR = (byte) 0xAA;
  /** Token type: Used to send an information message to the client. */
  static final byte TOKEN_INFO = (byte) 0xAB;
  /** Token type: Used to send a response to a login request (LOGIN7) to the client. */
  static final byte TOKEN_LOGINACK = (byte) 0xAD;
  /** Token type: Used to send a complete row, as defined by the COLMETADATA token, to the client. */
  static final byte TOKEN_ROW = (byte) 0xD1;
  /** Token type: A notification of an environment change (for example, database, language, and so on). */
  static final byte TOKEN_ENVCHANGE = (byte) 0xE3;
  /** Token type: Indicates the completion status of a command. */
  static final byte TOKEN_DONE = (byte) 0xFD;
  /** Token type: Indicates the completion status of commands within a stored procedure. */
  static final byte TOKEN_DONEPROC = (byte) 0xFE;
  /** Token type: Indicates the completion status of commands within a stored procedure. */
  static final byte TOKEN_DONEINPROC = (byte) 0xFF;
  /** Token type: Used to send the status value of an RPC to the client. */
  static final byte TOKEN_RETURNSTATUS = (byte) 0x79;
  /** Token type: Used to send the return value of an RPC to the client. */
  static final byte TOKEN_RETURNVALUE = (byte) 0xAC;
  /** Token type: Used to send the table name to the client only when in browser mode or from sp_cursoropen. */
  static final byte TOKEN_TABNAME = (byte) 0xA4;

  /**
   * Create a new reader instance.
   */
  protected TokenReader() {
  }

  /**
   * Create a new reader instance.
   *
   * @param stream byte stream to read the token from.
   */
  TokenReader(ByteBuffer stream) {
    assert stream != null;
    this.stream = stream;
    this.vldsReader = new VariableLengthDataStreamReader(stream);
    this.dtddsReader = new DataTypeDependentDataStreamReader(stream);
  }

  /**
   * Reads all tokens on the data stream.
   * 
   * @param input input stream that contains the token data stream.
   *
   * @return one collection with all the tokens read.
   */
  static TokenCollection readTokens(final ByteBuffer input) throws SQLException {
    int dataToRead = input.getInt();

    final TokenCollection tokens = new TokenCollection();
    TokenReader reader = null;

    while (dataToRead > 0) {
      byte token = input.get();
      dataToRead--;

      if (DevMode.concurrent_driver_sqlserver_tds) {
        DevMode.printlog("DataToRead=" + dataToRead + ", TOKEN = 0x" + Integer.toHexString(0x0FF & token));
      }

      switch (token) {
        case TOKEN_COLINFO:
          reader = new ColInfoTokenReader(input);
          tokens.addColInfo((ColInfoTokenReader) reader);
          break;
        case TOKEN_COLMETADATA:
          reader = new ColMetadataTokenReader(input);
          tokens.addColMetadata((ColMetadataTokenReader) reader);
          break;
        case TOKEN_DONE:
          reader = new DoneTokenReader(input);
          tokens.addDone((DoneTokenReader) reader);
          break;
        case TOKEN_DONEPROC:
          reader = new DoneProcTokenReader(input);
          tokens.addDoneProc((DoneProcTokenReader) reader);
          break;
        case TOKEN_DONEINPROC:
          reader = new DoneInProcTokenReader(input);
          tokens.addDoneInProc((DoneInProcTokenReader) reader);
          break;
        case TOKEN_ENVCHANGE:
          reader = new EnvChangeTokenReader(input);
          tokens.addEnvChange((EnvChangeTokenReader) reader);
          break;
        case TOKEN_ERROR:
          reader = new ErrorTokenReader(input);
          tokens.addError((ErrorTokenReader) reader);
          break;
        case TOKEN_INFO:
          reader = new InfoTokenReader(input);
          tokens.addInfo((InfoTokenReader) reader);
          break;
        case TOKEN_LOGINACK:
          reader = new LoginAckTokenReader(input);
          tokens.addLoginAck((LoginAckTokenReader) reader);
          break;
        case TOKEN_ROW:
          reader = new RowTokenReader(input, tokens.getColumnsMetadata());
          tokens.addRow((RowTokenReader) reader);
          break;
        case TOKEN_RETURNSTATUS:
          reader = new ReturnStatusTokenReader(input);
          tokens.addReturnStatus((ReturnStatusTokenReader) reader);
          break;
        case TOKEN_RETURNVALUE:
          reader = new ReturnValueTokenReader(input);
          tokens.addReturnValue((ReturnValueTokenReader) reader);
          break;
        case TOKEN_TABNAME:
          reader = new TabNameTokenReader(input);
          tokens.addTabName((TabNameTokenReader) reader);
          break;
        default:
          System.out.printf("UNKNOWN TOKEN = %x\n", token);
          reader = null;
      }
      if (reader != null) {
        try {
          dataToRead -= reader.read();
        } catch (IOException ex) {
          throw new SQLException(ex);
        }
        tokens.addReader(reader);
      }
    }
    tokens.throwExceptionIfErrorExists();

    return tokens;
  }

  /**
   * Read one token.
   *
   * @return number of bytes read.
   *
   * @throws IOException if an error occurs while reading from the data input stream.
   */
  abstract int read() throws IOException;
}
